<?php

/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Http
 * @subpackage Response
 * @version    $Id: Response.php 17124 2009-07-26 09:46:42Z shahar $
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

/**
 * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
 * includes easy access to all the response's different elemts, as well as some
 * convenience methods for parsing and validating HTTP responses.
 *
 * @package    Zend_Http
 * @subpackage Response
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Http_Response
{
	/**
	 * List of all known HTTP response codes - used by responseCodeAsText() to
	 * translate numeric codes to messages.
	 *
	 * @var array
	 */
	protected static $messages = array(
	// Informational 1xx
	100 => 'Continue',
	101 => 'Switching Protocols',

	// Success 2xx
	200 => 'OK',
	201 => 'Created',
	202 => 'Accepted',
	203 => 'Non-Authoritative Information',
	204 => 'No Content',
	205 => 'Reset Content',
	206 => 'Partial Content',

	// Redirection 3xx
	300 => 'Multiple Choices',
	301 => 'Moved Permanently',
	302 => 'Found',  // 1.1
	303 => 'See Other',
	304 => 'Not Modified',
	305 => 'Use Proxy',
	// 306 is deprecated but reserved
	307 => 'Temporary Redirect',

	// Client Error 4xx
	400 => 'Bad Request',
	401 => 'Unauthorized',
	402 => 'Payment Required',
	403 => 'Forbidden',
	404 => 'Not Found',
	405 => 'Method Not Allowed',
	406 => 'Not Acceptable',
	407 => 'Proxy Authentication Required',
	408 => 'Request Timeout',
	409 => 'Conflict',
	410 => 'Gone',
	411 => 'Length Required',
	412 => 'Precondition Failed',
	413 => 'Request Entity Too Large',
	414 => 'Request-URI Too Long',
	415 => 'Unsupported Media Type',
	416 => 'Requested Range Not Satisfiable',
	417 => 'Expectation Failed',

	// Server Error 5xx
	500 => 'Internal Server Error',
	501 => 'Not Implemented',
	502 => 'Bad Gateway',
	503 => 'Service Unavailable',
	504 => 'Gateway Timeout',
	505 => 'HTTP Version Not Supported',
	509 => 'Bandwidth Limit Exceeded'
	);

	/**
	 * The HTTP version (1.0, 1.1)
	 *
	 * @var string
	 */
	protected $version;

	/**
	 * The HTTP response code
	 *
	 * @var int
	 */
	protected $code;

	/**
	 * The HTTP response code as string
	 * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500)
	 *
	 * @var string
	 */
	protected $message;

	/**
	 * The HTTP response headers array
	 *
	 * @var array
	 */
	protected $headers = array();

	/**
	 * The HTTP response body
	 *
	 * @var string
	 */
	protected $body;

	/**
	 * HTTP response constructor
	 *
	 * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP
	 * response string and create a new Zend_Http_Response object.
	 *
	 * NOTE: The constructor no longer accepts nulls or empty values for the code and
	 * headers and will throw an exception if the passed values do not form a valid HTTP
	 * responses.
	 *
	 * If no message is passed, the message will be guessed according to the response code.
	 *
	 * @param int $code Response code (200, 404, ...)
	 * @param array $headers Headers array
	 * @param string $body Response body
	 * @param string $version HTTP version
	 * @param string $message Response code as text
	 * @throws Zend_Http_Exception
	 */
	public function __construct($code, $headers, $body = null, $version = '1.1', $message = null)
	{
		// Make sure the response code is valid and set it
		if (self::responseCodeAsText($code) === null) {
			require_once 'Zend/Http/Exception.php';
			throw new Zend_Http_Exception("{$code} is not a valid HTTP response code");
		}

		$this->code = $code;

		// Make sure we got valid headers and set them
		if (! is_array($headers)) {
			require_once 'Zend/Http/Exception.php';
			throw new Zend_Http_Exception('No valid headers were passed');
		}

		foreach ($headers as $name => $value) {
			if (is_int($name))
			list($name, $value) = explode(": ", $value, 1);

			$this->headers[ucwords(strtolower($name))] = $value;
		}

		// Set the body
		$this->body = $body;

		// Set the HTTP version
		if (! preg_match('|^\d\.\d$|', $version)) {
			require_once 'Zend/Http/Exception.php';
			throw new Zend_Http_Exception("Invalid HTTP response version: $version");
		}

		$this->version = $version;

		// If we got the response message, set it. Else, set it according to
		// the response code
		if (is_string($message)) {
			$this->message = $message;
		} else {
			$this->message = self::responseCodeAsText($code);
		}
	}

	/**
	 * Check whether the response is an error
	 *
	 * @return boolean
	 */
	public function isError()
	{
		$restype = floor($this->code / 100);
		if ($restype == 4 || $restype == 5) {
			return true;
		}

		return false;
	}

	/**
	 * Check whether the response in successful
	 *
	 * @return boolean
	 */
	public function isSuccessful()
	{
		$restype = floor($this->code / 100);
		if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ???
			return true;
		}

		return false;
	}

	/**
	 * Check whether the response is a redirection
	 *
	 * @return boolean
	 */
	public function isRedirect()
	{
		$restype = floor($this->code / 100);
		if ($restype == 3) {
			return true;
		}

		return false;
	}

	/**
	 * Get the response body as string
	 *
	 * This method returns the body of the HTTP response (the content), as it
	 * should be in it's readable version - that is, after decoding it (if it
	 * was decoded), deflating it (if it was gzip compressed), etc.
	 *
	 * If you want to get the raw body (as transfered on wire) use
	 * $this->getRawBody() instead.
	 *
	 * @return string
	 */
	public function getBody()
	{
		$body = '';

		// Decode the body if it was transfer-encoded
		switch (strtolower($this->getHeader('transfer-encoding'))) {

			// Handle chunked body
			case 'chunked':
				$body = self::decodeChunkedBody($this->body);
				break;

				// No transfer encoding, or unknown encoding extension:
				// return body as is
			default:
				$body = $this->body;
				break;
		}

		// Decode any content-encoding (gzip or deflate) if needed
		switch (strtolower($this->getHeader('content-encoding'))) {

			// Handle gzip encoding
			case 'gzip':
				$body = self::decodeGzip($body);
				break;

				// Handle deflate encoding
			case 'deflate':
				$body = self::decodeDeflate($body);
				break;

			default:
				break;
		}

		return $body;
	}

	/**
	 * Get the raw response body (as transfered "on wire") as string
	 *
	 * If the body is encoded (with Transfer-Encoding, not content-encoding -
	 * IE "chunked" body), gzip compressed, etc. it will not be decoded.
	 *
	 * @return string
	 */
	public function getRawBody()
	{
		return $this->body;
	}

	/**
	 * Get the HTTP version of the response
	 *
	 * @return string
	 */
	public function getVersion()
	{
		return $this->version;
	}

	/**
	 * Get the HTTP response status code
	 *
	 * @return int
	 */
	public function getStatus()
	{
		return $this->code;
	}

	/**
	 * Return a message describing the HTTP response code
	 * (Eg. "OK", "Not Found", "Moved Permanently")
	 *
	 * @return string
	 */
	public function getMessage()
	{
		return $this->message;
	}

	/**
	 * Get the response headers
	 *
	 * @return array
	 */
	public function getHeaders()
	{
		return $this->headers;
	}

	/**
	 * Get a specific header as string, or null if it is not set
	 *
	 * @param string$header
	 * @return string|array|null
	 */
	public function getHeader($header)
	{
		$header = ucwords(strtolower($header));
		if (! is_string($header) || ! isset($this->headers[$header])) return null;

		return $this->headers[$header];
	}

	/**
	 * Get all headers as string
	 *
	 * @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK")
	 * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
	 * @return string
	 */
	public function getHeadersAsString($status_line = true, $br = "\n")
	{
		$str = '';

		if ($status_line) {
			$str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}";
		}

		// Iterate over the headers and stringify them
		foreach ($this->headers as $name => $value)
		{
			if (is_string($value))
			$str .= "{$name}: {$value}{$br}";

			elseif (is_array($value)) {
				foreach ($value as $subval) {
					$str .= "{$name}: {$subval}{$br}";
				}
			}
		}

		return $str;
	}

	/**
	 * Get the entire response as string
	 *
	 * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
	 * @return string
	 */
	public function asString($br = "\n")
	{
		return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
	}

	/**
	 * Implements magic __toString()
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->asString();
	}

	/**
	 * A convenience function that returns a text representation of
	 * HTTP response codes. Returns 'Unknown' for unknown codes.
	 * Returns array of all codes, if $code is not specified.
	 *
	 * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown')
	 * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference
	 *
	 * @param int $code HTTP response code
	 * @param boolean $http11 Use HTTP version 1.1
	 * @return string
	 */
	public static function responseCodeAsText($code = null, $http11 = true)
	{
		$messages = self::$messages;
		if (! $http11) $messages[302] = 'Moved Temporarily';

		if ($code === null) {
			return $messages;
		} elseif (isset($messages[$code])) {
			return $messages[$code];
		} else {
			return 'Unknown';
		}
	}

	/**
	 * Extract the response code from a response string
	 *
	 * @param string $response_str
	 * @return int
	 */
	public static function extractCode($response_str)
	{
		preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m);

		if (isset($m[1])) {
			return (int) $m[1];
		} else {
			return false;
		}
	}

	/**
	 * Extract the HTTP message from a response
	 *
	 * @param string $response_str
	 * @return string
	 */
	public static function extractMessage($response_str)
	{
		preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m);

		if (isset($m[1])) {
			return $m[1];
		} else {
			return false;
		}
	}

	/**
	 * Extract the HTTP version from a response
	 *
	 * @param string $response_str
	 * @return string
	 */
	public static function extractVersion($response_str)
	{
		preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m);

		if (isset($m[1])) {
			return $m[1];
		} else {
			return false;
		}
	}

	/**
	 * Extract the headers from a response string
	 *
	 * @param string $response_str
	 * @return array
	 */
	public static function extractHeaders($response_str)
	{
		$headers = array();

		// First, split body and headers
		$parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
		if (! $parts[0]) return $headers;

		// Split headers part to lines
		$lines = explode("\n", $parts[0]);
		unset($parts);
		$last_header = null;

		foreach($lines as $line) {
			$line = trim($line, "\r\n");
			if ($line == "") break;

			if (preg_match("|^([\w-]+):\s+(.+)|", $line, $m)) {
				unset($last_header);
				$h_name = strtolower($m[1]);
				$h_value = $m[2];

				if (isset($headers[$h_name])) {
					if (! is_array($headers[$h_name])) {
						$headers[$h_name] = array($headers[$h_name]);
					}

					$headers[$h_name][] = $h_value;
				} else {
					$headers[$h_name] = $h_value;
				}
				$last_header = $h_name;
			} elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) {
				if (is_array($headers[$last_header])) {
					end($headers[$last_header]);
					$last_header_key = key($headers[$last_header]);
					$headers[$last_header][$last_header_key] .= $m[1];
				} else {
					$headers[$last_header] .= $m[1];
				}
			}
		}

		return $headers;
	}

	/**
	 * Extract the body from a response string
	 *
	 * @param string $response_str
	 * @return string
	 */
	public static function extractBody($response_str)
	{
		$parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
		if (isset($parts[1])) {
			return $parts[1];
		}
		return '';
	}

	/**
	 * Decode a "chunked" transfer-encoded body and return the decoded text
	 *
	 * @param string $body
	 * @return string
	 */
	public static function decodeChunkedBody($body)
	{
		$decBody = '';

		// If mbstring overloads substr and strlen functions, we have to
		// override it's internal encoding
		if (function_exists('mb_internal_encoding') &&
		((int) ini_get('mbstring.func_overload')) & 2) {

			$mbIntEnc = mb_internal_encoding();
			mb_internal_encoding('ASCII');
		}

		while (trim($body)) {
			if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
				require_once 'Zend/Http/Exception.php';
				throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message");
			}

			$length = hexdec(trim($m[1]));
			$cut = strlen($m[0]);
			$decBody .= substr($body, $cut, $length);
			$body = substr($body, $cut + $length + 2);
		}

		if (isset($mbIntEnc)) {
			mb_internal_encoding($mbIntEnc);
		}

		return $decBody;
	}

	/**
	 * Decode a gzip encoded message (when Content-encoding = gzip)
	 *
	 * Currently requires PHP with zlib support
	 *
	 * @param string $body
	 * @return string
	 */
	public static function decodeGzip($body)
	{
		if (! function_exists('gzinflate')) {
			require_once 'Zend/Http/Exception.php';
			throw new Zend_Http_Exception(
                'zlib extension is required in order to decode "gzip" encoding'
                );
		}

		return gzinflate(substr($body, 10));
	}

	/**
	 * Decode a zlib deflated message (when Content-encoding = deflate)
	 *
	 * Currently requires PHP with zlib support
	 *
	 * @param string $body
	 * @return string
	 */
	public static function decodeDeflate($body)
	{
		if (! function_exists('gzuncompress')) {
			require_once 'Zend/Http/Exception.php';
			throw new Zend_Http_Exception(
                'zlib extension is required in order to decode "deflate" encoding'
                );
		}

		/**
		 * Some servers (IIS ?) send a broken deflate response, without the
		 * RFC-required zlib header.
		 *
		 * We try to detect the zlib header, and if it does not exsit we
		 * teat the body is plain DEFLATE content.
		 *
		 * This method was adapted from PEAR HTTP_Request2 by (c) Alexey Borzov
		 *
		 * @link http://framework.zend.com/issues/browse/ZF-6040
		 */
		$zlibHeader = unpack('n', substr($body, 0, 2));
		if ($zlibHeader[1] % 31 == 0) {
			return gzuncompress($body);
		} else {
			return gzinflate($body);
		}
	}

	/**
	 * Create a new Zend_Http_Response object from a string
	 *
	 * @param string $response_str
	 * @return Zend_Http_Response
	 */
	public static function fromString($response_str)
	{
		$code    = self::extractCode($response_str);
		$headers = self::extractHeaders($response_str);
		$body    = self::extractBody($response_str);
		$version = self::extractVersion($response_str);
		$message = self::extractMessage($response_str);

		return new Zend_Http_Response($code, $headers, $body, $version, $message);
	}
}
